/**
 *  LastFM.cpp
 *
 *  Copyright (C) 2008  David Andrs <pda@jasnapaka.com>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "StdAfx.h"
#include "mLasq.h"
#include "LastFM.h"
#include "HttpClient.h"

#include "helpers.h"
#include "../libmad/mad.h"

#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif

const int CLastFM::SLOTS = 4;
const int CLastFM::SLOT_SIZE = 8192;

DWORD WINAPI ConnectProcStub(LPVOID lpParam) {
	CLastFM *lastFM = (CLastFM *) lpParam;
	lastFM->ConnectProc();
	return 0;
}

DWORD WINAPI StreamProcStub(LPVOID lpParam) {
	CLastFM *lastFM = (CLastFM *) lpParam;
	lastFM->StreamProc();
	return 0;
}

DWORD WINAPI PlayProcStub(LPVOID lpParam) {
	CLastFM *lastFM = (CLastFM *) lpParam;
	lastFM->PlayProc();
	return 0;
}

DWORD WINAPI GetPlaylistProcStub(LPVOID lpParam) {
	CLastFM *lastFM = (CLastFM *) lpParam;
	lastFM->GetPlaylistProc();
	return 0;
}

DWORD WINAPI GetCoverArtProcStub(LPVOID lpParam) {
	CLastFM *lastFM = (CLastFM *) lpParam;
	lastFM->GetCoverArtProc();
	return 0;
}

//////////////////////////////////////////////////////////////////////
// Construction/Destruction
//////////////////////////////////////////////////////////////////////

CLastFM::CLastFM() {
	HStreamThread = NULL;
	HPlayThread = NULL;
	HConnectThread = NULL;
	HGetPlaylistThread = NULL;
	HGetCoverArtThread = NULL;

	Authenticated = FALSE;
	State = Disconnected;

	Version = _T("1.3.1.1");			//

	Terminated = FALSE;
	HTerminate = CreateEvent(NULL, TRUE, FALSE, NULL);
	HNextTrack = CreateEvent(NULL, FALSE, FALSE, NULL);
	HBufferFull = CreateEvent(NULL, TRUE, FALSE, NULL);

	Buffer = new char [SLOTS * SLOT_SIZE];

/*	// FIXME: remove me
	// get actual file path
	TCHAR filePath[MAX_PATH];
	GetModuleFileName(NULL, filePath, MAX_PATH);
	TCHAR *bs = wcsrchr(filePath, '\\');
	if (bs != NULL) *bs = '\0';

//	CoverArtFileName.Format(_T("%s\\%s"), filePath, _T("cover_med.jpg"));
	CoverArtFileName.Format(_T("%s\\%s"), filePath, _T("3293397.jpg"));
*/
}

CLastFM::~CLastFM() {
	CloseHandle(HBufferFull);
	CloseHandle(HNextTrack);
	CloseHandle(HTerminate);
	DestroyPlaylist();
	delete ActivePlaylistEntry;
}

void CLastFM::DestroyPlaylist() {
	while (!Playlist.IsEmpty())
		delete Playlist.RemoveHead();
}

CString CLastFM::StateToString() {
	CString str;
	switch (State) {
		case Disconnected: str = _T("Disconnected"); break;
		case Connecting: str = _T("Connecting"); break;
		case Connected: str = _T("Connected"); break;
		case GettingPlaylist: str = _T("Getting playlist"); break;
		case Buffering: str = _T("Buffering"); break;
		case Playing: str = _T("Playing"); break;
		case Disconnecting: str = _T("Disconnecting"); break;
		case Error: str.Format(_T("Error: %s"), ErrorMsg); break;
		default: str = _T("Idle"); break;
	}

	return str;
}

void CLastFM::Connect(const CString &userName, const CString &password) {
	LOG2(1, "CLastFM::Connect('%S', '%S')", userName, password);

	UserName = userName;
	Password = password;

	HConnectThread = CreateThread(NULL, 0, ConnectProcStub, this, 0, NULL);
}

void CLastFM::Play() {
	LOG0(1, "CLastFM::Play()");

	HFillSemaphore = CreateSemaphore(NULL, 0, SLOTS, NULL);
	HEmptySemaphore = CreateSemaphore(NULL, SLOTS, SLOTS, NULL);

	ReadOffset = WriteOffset = 0;
	ActivePlaylistEntry = NULL;

	Terminated = FALSE;
	ResetEvent(HTerminate);
	ResetEvent(HNextTrack);
	ResetEvent(HBufferFull);
	EmptySlots = SLOTS;

	HStreamThread = CreateThread(NULL, 0, StreamProcStub, this, 0, NULL);
	HPlayThread = CreateThread(NULL, 0, PlayProcStub, this, 0, NULL);
}

void CLastFM::Stop() {
	LOG0(1, "CLastFM::Stop()");

	SetState(Disconnecting);
	Terminated = TRUE;
	SetEvent(HTerminate);
	SetEvent(HBufferFull);		// for the play thread to skip the WaitForSingleObject() call

	StreamConn.Close();
	CoverArtConn.Close();
}

BOOL CLastFM::ChangeStation(const CString &station) {
	LOG1(1, "CLastFM::ChangeStation('%s')", station);

	CString lastfmUrl;
	lastfmUrl.Format(_T("lastfm://artist/%s"), station);

	CString request;
	request.Format(_T("%s/adjust.php?session=%s&url=%s"), BasePath, Session, lastfmUrl);

	CHttpClient hc(BaseUrl);
	if (!hc.SendRequest(request))
		return FALSE;

	CNResponse response;
	if (!hc.ReceiveResponse(&response))
		return FALSE;

	if (response.GetResponseValue(_T("response")).CompareNoCase(_T("OK")) == 0) {
		// there will be a problem if the GettingPlaylist thread is running
		DestroyPlaylist();
		if (RequestXSPF()) {
			SetEvent(HNextTrack);
			return TRUE;
		}
		else
			return FALSE;
	}
	else
		return FALSE;
}

//
//
//

void CLastFM::SetState(EState state) {
	if (State != state) {
		State = state;
		AfxGetMainWnd()->SendMessage(UWM_STATE_CHANGED);		// let the main window know about it
	}
}

BOOL CLastFM::PerformHandshake() {
	LOG0(1, "CLastFM::PerformHandshake()");

	// do the handshake
	CString request;
	request.Format(_T("/radio/handshake.php?version=%s&platform=ppc&username=%s&passwordmd5=%s"), Version, UserName, Password);

	CHttpClient hc(_T("ws.audioscrobbler.com"), 80);
	if (!hc.SendRequest(request)) {
		ErrorMsg = _T("Sending request");
		return FALSE;
	}

	CNResponse response;
	if (!hc.ReceiveResponse(&response)) {
		ErrorMsg = _T("Receiving response");
		return FALSE;
	}

	// check the response
	CString session = response.GetResponseValue(_T("session"));

	if (session.IsEmpty() || session.CompareNoCase(_T("failed")) == 0) {
		ErrorMsg = _T("Incorrect user name or password");
		return FALSE;
	}

	// save important data
	Session = session;
	BaseUrl = response.GetResponseValue(_T("base_url"));
	BasePath = response.GetResponseValue(_T("base_path"));
	Authenticated = TRUE;

/*
	Session = _T("af7c4b575fe3cf59ff2cd496b73c6548");
	BaseUrl = _T("ws.audioscrobbler.com");
	BasePath = _T("/radio");
	Authenticated = TRUE;
*/
	return TRUE;
}

BOOL CLastFM::RequestXSPF() {
	LOG0(1, "CLastFM::RequestXSPF()");

	DestroyPlaylist();

	CString request;
	request.Format(_T("%s/xspf.php?sk=%s&discovery=0&desktop=%s"), BasePath, Session, Version);

	CHttpClient hc(BaseUrl);
	if (!hc.SendRequest(request)) {
		ErrorMsg = _T("Sending request");
		return FALSE;
	}

	CPlaylistResponse response;
	if (!hc.ReceiveResponse(&response)) {
		ErrorMsg = _T("Receiving response");
		return FALSE;
	}

	StationName = DecodeURL(response.Title);
	StationName.TrimLeft();
	StationName.TrimRight();

	char *st = WCharToChar(StationName);
	StationName = CharToWChar(st, strlen(st), CP_UTF8);
	delete [] st;

	// copy the playlist
	while (!response.Playlist.IsEmpty()) {
		CPlaylistEntry *pl = response.Playlist.RemoveHead();
		Playlist.AddTail(pl);
	}

	return TRUE;
}

void CLastFM::ConnectProc() {
	SetState(Connecting);

	if (PerformHandshake()) {
		SetState(Connected);
	}
	else {
		SetState(Disconnected);
	}
}

void CLastFM::StreamLoop() {
	LOG1(1, "- stream: %S", ActivePlaylistEntry->Location);
	DWORD serviceType;
	CString server, object;
	INTERNET_PORT port;
	ParseURL(ActivePlaylistEntry->Location, serviceType, server, object, port);

	if (StreamConn.Open(serviceType, server, port)) {
		LOG0(1, " - connection opened");
		CHttpRequest *req = StreamConn.CreateRequest(object);
		if (req != NULL) {
			LOG0(1, " - sending request");
			StreamConn.SendRequest(req);
			CHttpResponse *res = StreamConn.ReceiveResponse();
			if (res != NULL) {
				DWORD statusCode = res->GetStatusCode();
				if (statusCode == HTTP_STATUS_OK) {
					LOG0(1, " - good status");

					CString val;
					if (res->GetHeader(_T("Content-Length"), val)) {
						swscanf(val, _T("%d"), &Length);
						Length = Length / SLOT_SIZE;
						CurPos = 0;
					}
					AfxGetMainWnd()->PostMessage(UWM_UPDATE_METADATA);

					char buf[SLOT_SIZE] = { 0 };
					int numRead;

					while ((numRead = StreamConn.Read(buf + 0, sizeof(buf))) > 0) {
//						LOG1(1, " - got %d bytes", numRead);
						HANDLE handles[] = { HTerminate, HNextTrack, HEmptySemaphore };
						DWORD dwEvent = WaitForMultipleObjects(countof(handles), handles, FALSE, INFINITE);

						if (dwEvent == WAIT_OBJECT_0 + 2) {
							// put to the buffer
							memcpy(Buffer + WriteOffset, buf, SLOT_SIZE);
							WriteOffset = (WriteOffset + SLOT_SIZE) % (SLOTS * SLOT_SIZE);
							EmptySlots--;
							if (EmptySlots == 0) {
								SetState(Playing);
								SetEvent(HBufferFull);
							}

							ReleaseSemaphore(HFillSemaphore, 1, NULL);
						}
						else if (dwEvent == WAIT_OBJECT_0 + 1) {
							// next track
							// forget everything that was buffered, stop playing, reset semaphores
							SndDev.Reset();

							ResetEvent(HBufferFull);
							SetState(Buffering);

							CloseHandle(HFillSemaphore);
							HFillSemaphore = CreateSemaphore(NULL, 0, SLOTS, NULL);
							CloseHandle(HEmptySemaphore);
							HEmptySemaphore = CreateSemaphore(NULL, SLOTS, SLOTS, NULL);

							EmptySlots = SLOTS;
							ReadOffset = WriteOffset = 0;

							ResetEvent(HNextTrack);
							LOG0(1, "Next Track");
							break;
						}
						else
							break;		// terminate
					}
				}
				else {
					ErrorMsg.Format(_T("Stream response error %d"), statusCode);
					SetState(Error);
				}

				delete res;
			}
			else {
				ErrorMsg = _T("Receiving stream response");
				SetState(Error);
			}

			delete req;
		}
		else {
			ErrorMsg = _T("Sending stream request");
			SetState(Error);
		}

		StreamConn.Close();
	}
	else {
		ErrorMsg.Format(_T("Stream connection error"));
		SetState(Error);
	}
}

void CLastFM::StreamProc() {
	LOG0(1, "CLastFM::StreamProc() - start");

	//
	SetState(Buffering);

	if (RequestXSPF()) {
		while (!Playlist.IsEmpty() && !Terminated && State != Error) {
			ActivePlaylistEntry = Playlist.RemoveHead();

			GetCoverArt(ActivePlaylistEntry->Image);
			StreamLoop();

			delete ActivePlaylistEntry;
			ActivePlaylistEntry = NULL;

			if (Playlist.GetCount() == 1) {
				// last song of the playlist, get more playlist entries
				if (HGetPlaylistThread == NULL)
					HGetPlaylistThread = CreateThread(NULL, 0, GetPlaylistProcStub, this, 0, NULL);
			}
			else if (Playlist.GetCount() == 0 && HGetPlaylistThread != NULL) {
				// nothing to play, but getting the playlist is in the progress
				HANDLE handles[] = { HTerminate, HGetPlaylistThread };
				WaitForMultipleObjects(countof(handles), handles, FALSE, INFINITE);
				HGetPlaylistThread = NULL;
			}
		}
	}
	else {
		ErrorMsg = _T("Failed to get playlist");
		SetState(Error);
	}

	if (State != Error)
		SetState(Connected);

	CloseHandle(HStreamThread);
//	HStreamThread = NULL;

	CloseHandle(HEmptySemaphore);

	LOG0(1, "CLastFM::StreamProc() - end");
}

inline short f2s(mad_fixed_t f) {
	// A fixed point number is formed of the following bit pattern:
	// SWWWFFFFFFFFFFFFFFFFFFFFFFFFFFFF
	// MSB                          LSB
	// S ==> Sign (0 is positive, 1 is negative)
	// W ==> Whole part bits
	// F ==> Fractional part bits
	//
	// This pattern contains MAD_F_FRACBITS fractional bits, one
	// should alway use this macro when working on the bits of a fixed
	// point number. It is not guaranteed to be constant over the
	// different platforms supported by libmad.
	//
	// The signed short value is formed, after clipping, by the least
	// significant whole part bit, followed by the 15 most significant
	// fractional part bits. Warning: this is a quick and dirty way to
	// compute the 16-bit number, madplay includes much better
	// algorithms.

	// Clipping
	if (f >= MAD_F_ONE) return (SHRT_MAX);
	if (f <= -MAD_F_ONE) return (-SHRT_MAX);

	// Conversion
	f = f >> (MAD_F_FRACBITS - 15);
	return (signed short) f;
}

BOOL IsRecoverable(const mad_error &error) {
	if (MAD_RECOVERABLE(error)) {
		return TRUE;
	}
	else {
		if (error == MAD_ERROR_BUFLEN)
			return TRUE;
		else
			return FALSE;
	}
}

//#define INPUT_BUFFER_SIZE	(5*8192)
//#define OUTPUT_BUFFER_SIZE	8192 /* Must be an integer multiple of 4. */
#define OUTPUT_BUFFER_SIZE	8192 /* Must be an integer multiple of 4. */

void CLastFM::PlayProc() {
	LOG0(1, "CLastFM::PlayProc() - start");

	SndDev.Open();

	//
	struct mad_stream	stream;
	struct mad_frame	frame;
	struct mad_synth	synth;
	mad_timer_t			timer;

	unsigned char		input_buffer[2 * SLOT_SIZE + MAD_BUFFER_GUARD]; //INPUT_BUFFER_SIZE + MAD_BUFFER_GUARD],
	unsigned char		output_buffer[OUTPUT_BUFFER_SIZE],
						*output_ptr = output_buffer,
						*guard_ptr = NULL;
	const unsigned char	*output_buffer_end = output_buffer + OUTPUT_BUFFER_SIZE;


	// First the structures used by libmad must be initialized.
	mad_stream_init(&stream);
	mad_frame_init(&frame);
	mad_synth_init(&synth);
	mad_timer_reset(&timer);

	while (!Terminated) {
		DWORD dwResult = WaitForSingleObject(HBufferFull, INFINITE);
		if (dwResult != WAIT_OBJECT_0) break;

		if (stream.buffer == NULL || stream.error == MAD_ERROR_BUFLEN) {
			size_t read_size, remaining;
			unsigned char *read_start;

			/* {2} libmad may not consume all bytes of the input
			 * buffer. If the last frame in the buffer is not wholly
			 * contained by it, then that frame's start is pointed by
			 * the next_frame member of the Stream structure. This
			 * common situation occurs when mad_frame_decode() fails,
			 * sets the stream error code to MAD_ERROR_BUFLEN, and
			 * sets the next_frame pointer to a non NULL value. (See
			 * also the comment marked {4} bellow.)
			 *
			 * When this occurs, the remaining unused bytes must be
			 * put back at the beginning of the buffer and taken in
			 * account before refilling the buffer. This means that
			 * the input buffer must be large enough to hold a whole
			 * frame at the highest observable bit-rate (currently 448
			 * kb/s). XXX=XXX Is 2016 bytes the size of the largest
			 * frame? (448000*(1152/32000))/8
			 */
			if (stream.next_frame != NULL) {
				remaining = stream.bufend - stream.next_frame;
				memmove(input_buffer, stream.next_frame, remaining);
				read_start = input_buffer + remaining;
				read_size = SLOT_SIZE; // - remaining; //INPUT_BUFFER_SIZE - remaining;
			}
			else {
				read_size = SLOT_SIZE; //INPUT_BUFFER_SIZE;
				read_start = input_buffer;
				remaining = 0;
			}

			/* Fill-in the buffer. If an error occurs print a message
			 * and leave the decoding loop. If the end of stream is
			 * reached we also leave the loop but the return status is
			 * left untouched.
			 */
			HANDLE handles[] = { HTerminate, HFillSemaphore };
			DWORD dwEvent = WaitForMultipleObjects(countof(handles), handles, FALSE, INFINITE);
			if (dwEvent == WAIT_OBJECT_0) break;

			memcpy(read_start, Buffer + ReadOffset, SLOT_SIZE);
			ReadOffset = (ReadOffset + SLOT_SIZE) % (SLOTS * SLOT_SIZE);
			ReleaseSemaphore(HEmptySemaphore, 1, NULL);
			EmptySlots++;

			if (EmptySlots >= SLOTS) {
				LOG0(1, "Buffer empty");
				SetState(Buffering);
				ResetEvent(HBufferFull);
			}

			CurPos++;
			AfxGetMainWnd()->PostMessage(UWM_UPDATE_POS);

			/* {3} When decoding the last frame of a file, it must be
			 * followed by MAD_BUFFER_GUARD zero bytes if one wants to
			 * decode that last frame. When the end of file is
			 * detected we append that quantity of bytes at the end of
			 * the available data. Note that the buffer can't overflow
			 * as the guard size was allocated but not used the the
			 * buffer management code. (See also the comment marked
			 * {1}.)
			 *
			 * In a message to the mad-dev mailing list on May 29th,
			 * 2001, Rob Leslie explains the guard zone as follows:
			 *
			 *    "The reason for MAD_BUFFER_GUARD has to do with the
			 *    way decoding is performed. In Layer III, Huffman
			 *    decoding may inadvertently read a few bytes beyond
			 *    the end of the buffer in the case of certain invalid
			 *    input. This is not detected until after the fact. To
			 *    prevent this from causing problems, and also to
			 *    ensure the next frame's main_data_begin pointer is
			 *    always accessible, MAD requires MAD_BUFFER_GUARD
			 *    (currently 8) bytes to be present in the buffer past
			 *    the end of the current frame in order to decode the
			 *    frame."
			 */

			/* Pipe the new buffer content to libmad's stream decoder
             * facility.
			 */
			mad_stream_buffer(&stream, input_buffer, read_size + remaining);
			stream.error = MAD_ERROR_NONE;
		}

		// decode
		if (mad_frame_decode(&frame, &stream)) {
			if (IsRecoverable(stream.error))
				continue;
			else
				break;
		}

		mad_timer_add(&timer, frame.header.duration);

		// Once decoded the frame is synthesized to PCM samples. No errors
		// are reported by mad_synth_frame();
		mad_synth_frame(&synth, &frame);

		// Synthesized samples must be converted from libmad's fixed
		// point number to the consumer format. Here we use unsigned
		// 16 bit big endian integers on two channels. Integer samples
		// are temporarily stored in a buffer that is flushed when
		// full.
		for (int i = 0; i < synth.pcm.length; i++) {
			signed short sample;

			// Left channel
			sample = f2s(synth.pcm.samples[0][i]);
			*(output_ptr++) = sample & 0xff;
			*(output_ptr++) = sample >> 8;

			// Right channel. If the decoded stream is monophonic then
			// the right output channel is the same as the left one.
			if (MAD_NCHANNELS(&frame.header) == 2)
				sample = f2s(synth.pcm.samples[1][i]);
			*(output_ptr++) = sample & 0xff;
			*(output_ptr++) = sample >> 8;

			// Flush the output buffer if it is full.
			if (output_ptr == output_buffer_end) {
				SndDev.WriteAudio((char *) output_buffer + 0, OUTPUT_BUFFER_SIZE);
				output_ptr = output_buffer;
			}
		}
	}

	// Mad is no longer used, the structures that were initialized must
    // now be cleared.
	mad_synth_finish(&synth);
	mad_frame_finish(&frame);
	mad_stream_finish(&stream);

	SndDev.Close();

	CloseHandle(HPlayThread);
//	HPlayThread = NULL;

	CloseHandle(HFillSemaphore);

	LOG0(1, "CLastFM::PlayProc() - end");
}

void CLastFM::NextTrack() {
	SndDev.Pause();
	SetEvent(HNextTrack);
}

void CLastFM::GetPlaylistProc() {
	LOG0(1, "CLastFM::GetPlaylistProc() - start");

	if (RequestXSPF()) {
	}

	CloseHandle(HGetPlaylistThread);
	HGetPlaylistThread = NULL;

	LOG0(1, "CLastFM::GetPlaylistProc() - end");
}

void CLastFM::GetCoverArt(const CString &url) {
	// cancel possibly running cover art getting thread
	if (HGetCoverArtThread != NULL) {
		CoverArtConn.Close();
		WaitForSingleObject(HGetCoverArtThread, INFINITE);
	}

	CoverArtUrl = url;
	HGetCoverArtThread = CreateThread(NULL, 0, GetCoverArtProcStub, this, 0, NULL);
}

void CLastFM::GetCoverArtProc() {
	LOG0(1, "CLastFM::GetCoverArtProc() - start");

	CoverArtFileName.Empty();

	// get temp file name
	LPTSTR tempFileName = CoverArtFileName.GetBufferSetLength(MAX_PATH + 1);
	GetTempFileName(_T("\\Temp"), _T("msb"), 0, tempFileName);

	// download the cover art
	DWORD serviceType;
	CString server, object;
	INTERNET_PORT port;
	ParseURL(CoverArtUrl, serviceType, server, object, port);
	LOG1(1, "GetCoverArt from: %S", CoverArtUrl);

	if (CoverArtConn.Open(serviceType, server, port)) {
		CHttpRequest *req = CoverArtConn.CreateRequest(object);
		if (req != NULL) {
			CoverArtConn.SendRequest(req);
			CHttpResponse *res = CoverArtConn.ReceiveResponse();
			if (res != NULL) {
				DWORD statusCode = res->GetStatusCode();
				if (statusCode == HTTP_STATUS_OK) {
					char buf[SLOT_SIZE] = { 0 };
					int numRead;

					CString val;
					DWORD remain = 0;
					if (res->GetHeader(_T("Content-Length"), val))
						swscanf(val, _T("%d"), &remain);

					LOG1(1, "TmpFile: %S", tempFileName);
					HANDLE hFile = CreateFile(tempFileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
					if (hFile != INVALID_HANDLE_VALUE) {
						// get the data from the server
						while (remain > 0 && (numRead = CoverArtConn.Rd(buf + 0, sizeof(buf))) > 0) {
							DWORD written;
							WriteFile(hFile, buf, numRead, &written, NULL);
							remain -= numRead;
						}

						CloseHandle(hFile);

						// notify the main window that we have a cover art to update
						AfxGetMainWnd()->SendMessage(UWM_UPDATE_COVERART);

						// clean
						DeleteFile(tempFileName);
					}
				}

				delete res;
			}

			delete req;
		}

		CoverArtConn.Close();
	}


	// end it
	CloseHandle(HGetCoverArtThread);
	HGetCoverArtThread = NULL;

	LOG0(1, "CLastFM::GetCoverArtProc() - end");
}
